/*******************************************************************************
 * Copyright (c) 2014 Francesco Gabbrielli (http://www.francescogabbrielli.it).
 * 
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Contributors:
 *     Francesco Gabbrielli - initial API and implementation
 ******************************************************************************/
package it.francescogabbrielli.android.solidalapp;

import com.google.android.gcm.server.Constants;
import com.google.android.gcm.server.MulticastResult;
import com.google.android.gcm.server.Result;
import com.google.android.gcm.server.Sender;
import com.google.api.server.spi.config.ApiMethod;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
 * Non persistence endpoint, used only to send a "device to device" message 
 */
public class MessageEndpoint {
		
	/** GCM API key */
	protected static final String API_KEY = "AIzaSyCR7lNV7PlkbkFL73sgV3J3U_fY1sm1Zjk";
	
	/** Maximum message length */
	private final static int MAX_TEXT_LENGTH = 400;

	/** Server logger */
	protected static final Logger logger = Logger.getLogger(MessageEndpoint.class.getName());
		
	protected DeviceEndpoint deviceEndpoint = new DeviceEndpoint();
	protected ProfileEndpoint profileEndpoint = new ProfileEndpoint();
	
	/**
	 * Used to send a message via GCM to a specific device without
	 * persistence.
	 * 
	 * Usage is slightly different from that of a normal endpoint:
	 * <ul>
	 *  <li>put the message into message</li>
	 *  <li>put the target device into registrationId</li>
	 *  <li>put optional sender info into sender and senderId</li>
	 * </ul>
	 * 
	 * @param message the message to send
	 * @return the message sent
	 */
	@ApiMethod(name = "insertMessage")
	public Message insertMessage(Message message) {
		try {
			Result result = doSendViaGcm(message);
			if (result.getErrorCodeName()!=null) {
				message.setError(result.getErrorCodeName());
				logger.warning("Message not sent: "+result.getErrorCodeName());
			}
		} catch(Exception e) {
			message.setError(""+e.getLocalizedMessage());
			logger.severe("Cannot send message: "+e);
		}
		return message;
	}
	
	/**
	 * Build message for GCM
	 * 
	 * @param id
	 * 		the id of the message
	 * @param message
	 * 		the text
	 * @param type
	 * 		the type of the message (optional)
	 * @return
	 */
	private com.google.android.gcm.server.Message buildMessage(Message message, Map<String, String> fields) {
		
		String text = message.getMessage();
		
		// Trim message if needed
		if (text.length() > MAX_TEXT_LENGTH)
			text = text.substring(0, MAX_TEXT_LENGTH-6) + " [...]";
		
		// Set expiration if appropriate
		int timeToLive = 0;
		if (message instanceof Request)
			timeToLive = ((Request) message).getDuration() * 60;
		
		// Set sender if not null
		String sender = "";
		if (message.getSender()!=null)
			sender = message.getSender();

		com.google.android.gcm.server.Message.Builder msgBuilder = 
				new com.google.android.gcm.server.Message.Builder()
			.addData(Message.ID, String.valueOf(message.getId()))
			.addData(Message.MESSAGE, text)
			.addData(Message.SENDER, sender)
			.addData(Message.SENDER_ID, String.valueOf(message.getSenderId()))
			.addData(Message.DEVICE_ID, String.valueOf(message.getDeviceId()));
		if (fields!=null)
			for (String field : fields.keySet())
				msgBuilder.addData(field, fields.get(field));
		if (timeToLive>0)
			msgBuilder.timeToLive(timeToLive);
		return msgBuilder.build();
	}
	
	/** 
	 * Internal usage only for sending a non persisted message
	 *  
	 * @param message the message to send
	 * @return the result 
	 */
	private Result doSendViaGcm(Message message) throws IOException {
		return doSendViaGcm(message, getClass().getSimpleName().toLowerCase());
	}
	
	/** 
	 * Internal usage only for sending a non persisted message
	 *  
	 * @param message the message to send
	 * @return the result 
	 */
	protected Result doSendViaGcm(Message message, String type) throws IOException {
		Device device = deviceEndpoint.getDevice(message.getDeviceId());
		return doSendViaGcm(message, type, device);
	}

	/**
	 * Send a message to the specified registered devices 
	 * 
	 * @param id
	 * 		the id of the request just inserted 
	 * @param message
	 *      the message to be sent in the GCM ping to the devices.
	 * @param device
	 *      the device to send message to
	 * @param type
	 * 		the type of the message
	 * @return 
	 * 		the result of the ping.
	 */
	protected Result doSendViaGcm(Message message, String type, Device device) throws IOException {
		HashMap<String, String> fields = new HashMap<String, String>();
		fields.put(Message.TYPE, type);
		return doSendViaGcm(message, fields, device);
	}
	
	/**
	 * Send a message to the specified registered devices 
	 * 
	 * @param id
	 * 		the id of the request just inserted 
	 * @param message
	 *      the message to be sent in the GCM ping to the devices.
	 * @param device
	 *      the device to send message to
	 * @param fields
	 * 		the additional fields to put in the message payload
	 * @return 
	 * 		the result of the ping.
	 */
	protected Result doSendViaGcm(Message message, Map<String, String> fields, Device device) 
	throws IOException {
		
		Sender sender = new Sender(API_KEY);
		
		logger.info("Sending message from "+message.getSender()+" to "+device.getRegistrationId());
		Result result = sender.sendNoRetry(
				buildMessage(message, fields), device.getRegistrationId());
		logger.info("Message '"+fields+"' n."+message.getId()+" sent: "+message.getMessage());
		
		try {
			checkDevice(device, result, message);
		} catch(Exception e) {
			logger.severe("Error checking device: "+e);
		}
		
		return result;
	}


	/**
	 * Send a multicast message to the specified registered devices, with geolocation
	 * 
	 * @param id
	 * 		the id of the request just inserted 
	 * @param message
	 *      the message to be sent in the GCM ping to the devices.
	 * @param devices
	 *      the devices.
	 * @return the result of the ping.
	 */
	protected MulticastResult doSendViaGcm(Message message, Collection<Device> devices)
	throws IOException {

		Sender sender = new Sender(API_KEY);
		
		logger.info("GCM Sending messages to "+(devices.size()-1)+" devices");
		
		List<String> regIds = new LinkedList<String>();
		for (Iterator<Device> it = devices.iterator(); it.hasNext();) {
			Device info = it.next();
			if(info.getId() != message.getDeviceId())
				regIds.add(info.getRegistrationId());
			else
				it.remove();
		}
		
		Map<String, String> fields = new HashMap<String, String>();
		fields.put("lon", String.valueOf(message.getLongitude()));
		fields.put("lat", String.valueOf(message.getLatitude()));
		MulticastResult results = sender.sendNoRetry(buildMessage(message, fields), regIds);
		logger.info("GCM Messages sent: "+(results!=null ? results.getSuccess() : 0));

		try {
			Iterator<Device> it = devices.iterator();
			for (Result result : results.getResults())
				checkDevice(it.next(), result, message);
		} catch(IOException e) {
			logger.severe("Error checking devices: "+e);
			throw e;
		}
		
		return results;
	}
	
	/**
	 * Check device after sending
	 * 
	 * @param device
	 * 
	 * @param result
	 * 
	 * @param message
	 * 
	 */
	private void checkDevice(Device device, Result result, Message message) throws IOException {
		String regId = device.getRegistrationId();
		if (message.getId()>0)
			if (result.getMessageId() != null) {
				String canonicalRegId = result.getCanonicalRegistrationId();
				if (canonicalRegId!=null && !canonicalRegId.equals(regId)) {
					logger.info("Change registration:\n"+regId+"\n=>"+canonicalRegId);
					
					// send an "update registration" message
					Message m = new Message();
					m.setMessage(canonicalRegId);
					Map<String, String> fields = new HashMap<String, String>();
					fields.put(Message.TYPE, Message.TYPE_REG_UPDATE);
					try {
						Sender sender = new Sender(API_KEY);
						sender.send(buildMessage(message, fields), regId, 3);
					} catch(IOException e) {
						logger.warning("Cannot send registration update message: "+e);
						e.printStackTrace();
						throw e;
					}
					
					//update device??? old regId, will still work?
					if (message.getDeviceId()!=device.getId().longValue())
						deviceEndpoint.removeDevice(device.getId());
				}
			} else {
				String error = result.getErrorCodeName();
				if (error.equals(Constants.ERROR_NOT_REGISTERED)) {
					logger.info("Not registered: "+regId);
					deviceEndpoint.removeDevice(message.getDeviceId());
				}
			}
	}

}
